package whf.framework.workflow.engine;

import java.io.InputStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.hibernate.Query;
import org.hibernate.Session;
import org.jbpm.JbpmConfiguration;
import org.jbpm.JbpmContext;
import org.jbpm.JbpmException;
import org.jbpm.context.exe.ContextInstance;
import org.jbpm.db.GraphSession;
import org.jbpm.db.TaskMgmtSession;
import org.jbpm.graph.def.Node;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.graph.exe.Token;
import org.jbpm.taskmgmt.def.Task;
import org.jbpm.taskmgmt.exe.TaskInstance;

import whf.framework.log.Log;
import whf.framework.log.LogFactory;
import whf.framework.util.StringUtils;
import whf.framework.workflow.entity.WorkflowDefinition;
import whf.framework.workflow.entity.WorkflowEntity;
import whf.framework.workflow.exception.WorkflowException;

/**
 * @author King
 *
 */
@SuppressWarnings("unchecked")
public class WorkflowEngineImpl implements WorkflowEngine {
	protected static final Log log = LogFactory.getLog(WorkflowEngineImpl.class);
	protected static final String JBPM_CFG_XML = "jbpm.cfg.xml";

	public void createSchema() {
		this.getConfiguration().createSchema();
	}

	public void dropSchema() {
		this.getConfiguration().dropSchema();
	}
	
	public JbpmConfiguration getConfiguration() {
		return JbpmConfiguration.getInstance(JBPM_CFG_XML);
	}
	
	
	public JbpmContext getContext() {
		return this.getConfiguration().getCurrentJbpmContext();
	}
	// ProcessDefinition
	
	public ProcessDefinition getProcessDefinition(long processDefinitionId) {
		JbpmContext ctx = this.getContext();
		GraphSession graphSession = ctx.getGraphSession();
		ProcessDefinition pdef = getProcessDefinition(processDefinitionId, graphSession);
		return pdef;
	}

	private ProcessDefinition getProcessDefinition(long processDefinitionId, GraphSession session) {
		return session.getProcessDefinition(processDefinitionId);
	}

	public ProcessDefinition getProcessDefinitionByName(String name) {
		JbpmContext ctx = this.getContext();
		GraphSession graphSession = ctx.getGraphSession();
		ProcessDefinition pdef = graphSession.findLatestProcessDefinition(name);
		return pdef;
	}
	
	public boolean isDefinitionDeployed(long processDefinitionId) throws WorkflowException {
		return this.getProcessDefinition(processDefinitionId) != null;
	}

	public ProcessDefinition deployDefinition(InputStream stream) throws WorkflowException {
		if (stream == null) {
			throw new WorkflowException("Not definition provided !");
		}
		ProcessDefinition processDefinition = ProcessDefinition.parseXmlInputStream(stream);
		if (!isDefinitionDeployed(processDefinition.getId())) {
			JbpmContext ctx = getConfiguration().getCurrentJbpmContext();
			try {
				ctx.deployProcessDefinition(processDefinition);
			} catch (JbpmException jbpme) {
				throw new WorkflowException("An error occurred while trying to deploy the process definition !", jbpme);
			}
		} else {
			log.info("Definition already deployed !");
		}
		return processDefinition;
	}

	public void undeployDefinition(ProcessDefinition pdef) throws WorkflowException {
		JbpmContext ctx = getConfiguration().getCurrentJbpmContext();
		GraphSession graphSession = ctx.getGraphSession();
		try {
			graphSession.deleteProcessDefinition(pdef.getId());
		} catch (JbpmException jbpme) {
			throw new WorkflowException("Error while attempting to undeploy workflow definition with id=" + pdef.getName(), jbpme);
		}
	}
	
	//
	public ProcessInstance startProcessInstance(ProcessDefinition processDefinition, WorkflowEntity entity, Map<String, Object> attrs)
			throws WorkflowException {
		JbpmContext jbpmContext = this.getContext();

		if (processDefinition == null) {
			WorkflowDefinition definition = entity.getWorkflowDefinition();
			if(definition == null) {
				throw new WorkflowException("Workflow definition can not be null!");
			}
			if(definition.getCurrentVersion() != null){
				definition = definition.getCurrentVersion();
			}
			long processDefinitionId = definition.getProcessDefinition();
			processDefinition = this.getProcessDefinition(processDefinitionId);
		}

		ProcessInstance processInstance = new ProcessInstance(processDefinition);

		setProcessParameters(jbpmContext, processInstance, attrs);

		Task startTask = processInstance.getTaskMgmtInstance().getTaskMgmtDefinition().getStartTask();
		TaskInstance startTaskInstance = null;
		if (startTask != null) {
			startTaskInstance = processInstance.getTaskMgmtInstance().createStartTaskInstance();
			if (attrs != null && attrs.containsKey(WorkflowConstants.WORKFLOW_CREATOR)) {
				jbpmContext.setActorId((String) attrs.get(WorkflowConstants.WORKFLOW_CREATOR));
			}
			startTaskInstance.end();
		} else {
			processInstance.signal();
		}
		jbpmContext.save(processInstance);
		//
		entity.setProcessInstanceId(processInstance.getId());
		try{
			WorkflowEntityService.saveWorkflowEntity(entity);
		}catch(Exception e){
			throw new WorkflowException(e);
		}
		//
		return processInstance;
	}

	public ProcessInstance terminateProcessInstance(ProcessInstance processInstance) throws WorkflowException {
		if (processInstance == null) {
			throw new WorkflowException("Process Instance must not be null!");
		}
		processInstance.end();
		return processInstance;
	}

	public ProcessInstance getProcessInstance(long pid) throws WorkflowException {
		ProcessInstance pi = null;
		JbpmContext ctx = getConfiguration().getCurrentJbpmContext();
		try {
			pi = ctx.getGraphSession().getProcessInstance(pid);
			if (pi == null) {
				log.error("Cannot find process instance with id=" + pid);
			}
		} catch (JbpmException jbpme) {
			throw new WorkflowException(jbpme);
		} catch (Exception e) {
			throw new WorkflowException(e);
		}
		return pi;
	}

	public List<ProcessInstance> findProcessInstancesByProcessDefinition(ProcessDefinition processDefinition) {
		JbpmContext ctx = getConfiguration().getCurrentJbpmContext();
		GraphSession session = ctx.getGraphSession();

		List<ProcessInstance> processInstances = session.findProcessInstances(processDefinition.getId());

		return processInstances;
	}

	public List<ProcessInstance> findProcessInstancesByActor(String actorId) {
		JbpmContext ctx = getConfiguration().getCurrentJbpmContext();

		Session session = ctx.getSession();
		Query query = session.createQuery("select pi from org.jbpm.graph.exe.ProcessInstance as pi where pi.key like :actorId "
				+ "order by pi.start desc");
		query.setString("actorId", "%|" + actorId + "|%");
		List<ProcessInstance> processInstances = query.list();

		return processInstances;
	}

    public TaskInstance getTaskInstance(long taskInstanceId) throws WorkflowException{
    	JbpmContext ctx = this.getContext();
    	TaskMgmtSession taskSession=ctx.getTaskMgmtSession();
        try {
            return taskSession.getTaskInstance(taskInstanceId);
        } catch (JbpmException jbpme) {
            throw new WorkflowException(jbpme);
        }
    }

	public TaskInstance startTaskInstance(TaskInstance taskInstance) throws WorkflowException {
		JbpmContext ctx = getConfiguration().getCurrentJbpmContext();
		try {
			if (taskInstance != null) {
				taskInstance.start();
				if (taskInstance.getStart() == null) {
					ProcessInstance pi = taskInstance.getToken().getProcessInstance();
					ctx.save(pi);
				}
			} else {
				throw new WorkflowException("Task with identifier=" + taskInstance.getId() + " is not found ...");
			}
		} catch (JbpmException jbpme) {
			throw new WorkflowException(jbpme);
		}
		return taskInstance;
	}
	
	public TaskInstance suspendTaskInstance(TaskInstance taskInstance) throws WorkflowException {
		JbpmContext ctx = getConfiguration().getCurrentJbpmContext();
		try {
			if (taskInstance != null) {
				taskInstance.suspend();
				if (taskInstance.getStart() == null) {
					ProcessInstance pi = taskInstance.getToken().getProcessInstance();
					ctx.save(pi);
				}
			} else {
				throw new WorkflowException("Task with identifier=" + taskInstance.getId() + " is not found ...");
			}
		} catch (JbpmException jbpme) {
			throw new WorkflowException(jbpme);
		}
		return taskInstance;
	}

	public TaskInstance endTaskInstance(TaskInstance taskInstance, String transitionName) throws WorkflowException {
		JbpmContext ctx = getConfiguration().getCurrentJbpmContext();

		if (taskInstance.hasEnded()) {
			return taskInstance;
		}

//		if (taskInstance.isRejected()) {
//			taskInstance.setRejected(false);
//		}

		if (transitionName == null) {
			try {
				taskInstance.end();
			} catch (IllegalStateException jbpme) {
				throw new WorkflowException(jbpme);
			}
		} else {
			Node node = taskInstance.getToken().getNode();
			if (node.getLeavingTransition(transitionName) == null) {
				throw new WorkflowException("Cannot find transition=" + transitionName);
			} else {
				try {
					taskInstance.end(transitionName);
				} catch (IllegalStateException jbpme) {
					throw new WorkflowException(jbpme);
				}
			}
		}

		ProcessInstance pi = taskInstance.getToken().getProcessInstance();
		ctx.save(pi);
		return taskInstance;
	}

	public TaskInstance updateTaskInstance(TaskInstance taskInstance, Map<String, String> props) throws WorkflowException {
		throw new WorkflowException("Not supported yet!");
	}

	public void assignTaskInstance(TaskInstance taskInstance, String participant) {
		JbpmContext ctx = this.getContext();

		ExecutionContext eCtx = new ExecutionContext(taskInstance.getToken());
		eCtx.setVariable(WorkflowConstants.WORKFLOW_TASK_ASSIGNEE, participant);
		
		taskInstance.setActorId(participant);

		ProcessInstance pi = taskInstance.getToken().getProcessInstance();
		ctx.save(pi);
	}

	public void unAssignTaskInstance(TaskInstance taskInstance, String participant) {
		JbpmContext ctx = this.getContext();
		taskInstance.setActorId(null);

		ProcessInstance pi = taskInstance.getToken().getProcessInstance();
		ctx.save(pi);
	}
	
	public Collection<TaskInstance> findTaskInstancesByProcessInstance(ProcessInstance processInstance) throws WorkflowException {
		Collection<TaskInstance> tasks = processInstance.getTaskMgmtInstance().getTaskInstances();
		for(Iterator<TaskInstance> it = tasks.iterator(); it.hasNext(); ) {
			TaskInstance task = it.next();
			if(task.hasEnded() || task.isCancelled()) {
				it.remove();
			}
		}
		return tasks;
	}
	
	public Collection<TaskInstance> findTaskInstancesByActor(String actorId) throws WorkflowException {
		Collection<TaskInstance> tasks = null;
		JbpmContext ctx = this.getContext();
		
		Session session = ctx.getSession();
		try {
			Query query = session.getNamedQuery("TaskMgmtSession.findAllTaskInstancesByActorId");
			query.setString("actorId", actorId);
			tasks = query.list();
		} catch (Exception e) {
			throw new WorkflowException(e);
		}

		return tasks;
	}

	public Collection<TaskInstance> findTaskInstancesByActorAndProcessNode(String actorId, String processName, String nodeName) throws WorkflowException {
		Collection<TaskInstance> tasks = this.findTaskInstancesByActor(actorId);
		for(Iterator<TaskInstance> it = tasks.iterator(); it.hasNext(); ) {
			TaskInstance taskInstance = it.next();
			if(!StringUtils.equals(processName, taskInstance.getProcessInstance().getProcessDefinition().getName()) || 
					!StringUtils.equals(nodeName, taskInstance.getTask().getTaskNode().getName())) {
				it.remove();
			}
		}
		return tasks;
	}

	public Token followTransition(Token curToken, String transitionName, Map<String, Object> attrs) throws WorkflowException {

		JbpmContext ctx = getConfiguration().getCurrentJbpmContext();
		GraphSession graphSession = ctx.getGraphSession();

		ProcessInstance pi;
		try {
			pi = graphSession.getProcessInstance(curToken.getProcessInstance().getId());
		} catch (JbpmException jbpme) {
			throw new WorkflowException("Cannot find process instance...!", jbpme);
		}

		if (pi == null) {

			return null;
		}

		setProcessParameters(ctx, pi, attrs);

		Token token = pi.findToken(curToken.getFullName());
		if (token == null) {
			throw new WorkflowException("Token not found !");
		}

		if (transitionName == null) {
			token.signal();
		} else {
			Node node = token.getNode();
			if (node.hasNoLeavingTransitions()) {
				throw new WorkflowException("No leaving transitions found for !");
			}
			token.signal(transitionName);
		}

		pi = token.getProcessInstance();
		ctx.save(pi);

		return token;
	}

	private void setProcessParameters(JbpmContext ctx, ProcessInstance pi, Map<String, Object> attrs) {
		if (attrs != null) {
			ContextInstance ctxInstance = pi.getContextInstance();
			Iterator<String> keyIt = attrs.keySet().iterator();
			while (keyIt.hasNext()) {
				String key = keyIt.next();
				Object value = attrs.get(key);
				if (key.equals(WorkflowConstants.WORKFLOW_CREATOR)) {
					try {
						ctx.setActorId((String) value);
					} catch (ClassCastException cce) {
						log.error("Impossible to get the keys for actorId...");
					}
				}
				ctxInstance.setVariable(key, attrs.get(key));
			}
		}
	}

}
